﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace ERF
{
    /// <summary>
    /// Represents a camera.
    /// </summary>
    /// 
    public class Camera : GameComponent, ICameraService {
        public const float MOVEMENT_SPEED = 0.005f;
        private const float DEFAULT_NEAR_CLIPPINGPLANE = 0.001f;
        private const float DEFAULT_FAR_CLIPPINGPLANE = 1000.0f;
        private Matrix view, projection, world;
        private float fieldOfView;
        private float aspectRatio;
        private float nearPlaneDistance;
        private float farPlaneDistance;

        private Vector3 cameraTarget;
        private Vector3 cameraPosition;
        private Vector3 cameraUp;
        private Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
        private Vector3 cameraOriginalUp = new Vector3(0, 1, 0);
        private Vector2 cameraRotation;

        private bool projectionIsDirty;
        private bool viewIsDirty;
        private bool worldIsDirty;

        private bool suppressViewUpdating = false;

        private Matrix reflectionViewMatrix;
        public Vector3 reflectionPosition;

        public bool useReflectionValues;

        public event EventHandler PositionChanged;

        public Matrix ReflectionViewMatrix {
            get { return reflectionViewMatrix; }
            set { reflectionViewMatrix = value; }
        }
        const float waterHeight = 5.0f;

        /// <summary>
        /// Creates a new Camera instance with default values.
        /// </summary>
        /// <param name="game">The game this instance belongs to.</param>
        /// <param name="aspectRatio">The aspectratio of the viewport.</param>
        /// <param name="cameraPosition">The position of the camera.</param>
        /// <param name="cameraRotation">The rotation of the camera.</param>
        public Camera(
            ERFGame game,
            float aspectRatio,
            Vector3 cameraPosition,
            Vector2 cameraRotation
            )
            : this(game,MathHelper.PiOver4, aspectRatio, DEFAULT_NEAR_CLIPPINGPLANE, DEFAULT_FAR_CLIPPINGPLANE, cameraPosition, cameraRotation, Vector3.Up) {
        }

        /// <summary>
        /// Creates a new Camera instance with the specified parameters.
        /// </summary>
        /// <param name="game">The game this instance belongs to.</param>
        /// <param name="fieldOfView">The field of view of this camera, in radians.</param>
        /// <param name="aspectRatio">The aspectratio of the viewport.</param>
        /// <param name="nearPlaneDistance">The near plane distance.</param>
        /// <param name="farPlaneDistance">The far plane distance.</param>
        /// <param name="cameraPosition">The position of the camera.</param>
        /// <param name="cameraRotation">The rotation of the camera.</param>
        /// <param name="cameraUp">The direction that points straight up, relative to the camera.</param>
        public Camera(
            ERFGame game,
            float fieldOfView,
            float aspectRatio,
            float nearPlaneDistance,
            float farPlaneDistance,
            Vector3 cameraPosition,
            Vector2 cameraRotation,
            Vector3 cameraUp
            ) : base(game) {
            this.fieldOfView = fieldOfView;
            this.aspectRatio = aspectRatio;
            this.nearPlaneDistance = nearPlaneDistance;
            this.farPlaneDistance = farPlaneDistance;
            this.cameraPosition = cameraPosition;
            this.cameraRotation = cameraRotation;
            this.cameraUp = cameraUp;
            this.viewIsDirty = false;
            this.projectionIsDirty = false;
            this.worldIsDirty = true;

            this.CreateProjection();
            this.CreateView();

        }

        /// <summary>
        /// Creates the world matrix.
        /// </summary>
        private void CreateWorld()
        {
            Matrix translation, rotation;
            Matrix.CreateTranslation(ref this.cameraPosition, out translation);
            Matrix.CreateRotationY(this.cameraRotation.Y, out rotation);
            this.world = rotation * translation;
            this.worldIsDirty = false;
        }

        /// <summary>
        /// Create the projection matrix.
        /// </summary>
        private void CreateProjection() {
            Matrix.CreatePerspectiveFieldOfView(this.fieldOfView, this.aspectRatio, this.nearPlaneDistance, this.farPlaneDistance, out this.projection);
            this.projectionIsDirty = false;
        }
        
        /// <summary>
        /// Create the view matrix.
        /// </summary>
        private void CreateView() {
            if(!this.suppressViewUpdating){
                Matrix cameraRotation = Matrix.CreateRotationX(this.cameraRotation.X) * Matrix.CreateRotationY(this.cameraRotation.Y);

                Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
                Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);
                Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
                Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget;
                Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);

                this.view = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);

                this.reflectionPosition = cameraPosition;
                this.reflectionPosition.Y = -cameraPosition.Y + waterHeight * 2;
                Vector3 reflTargetPos = cameraFinalTarget;
                reflTargetPos.Y = -cameraFinalTarget.Y + waterHeight * 2;

                Vector3 cameraRight = Vector3.Transform(new Vector3(1, 0, 0), cameraRotation);
                Vector3 invUpVector = Vector3.Cross(cameraRight, reflTargetPos - this.reflectionPosition);

                this.reflectionViewMatrix = Matrix.CreateLookAt(this.reflectionPosition, reflTargetPos, invUpVector);
                this.cameraTarget = cameraRotatedTarget;
            }
            
        }

        /// <summary>
        /// Forces an update of the view matrix.
        /// </summary>
        public void ForceViewUpdate() {
            this.CreateView();
        }

        /// <summary>
        /// Gets or sets a value determining if the updating of the view matrix should be suppressed.
        /// </summary>
        public bool SuppressViewUpdating {
            get { return this.suppressViewUpdating; }
            set { this.suppressViewUpdating = value; }
        }

        /// <summary>
        /// Gets the world matrix.
        /// </summary>
        public Matrix World {
            get {
                if (this.worldIsDirty)
                    this.CreateWorld();
                return this.world; 
            }
        }

        /// <summary>
        /// Gets or sets the aspect ratio.
        /// </summary>
        public float AspectRatio {
            get { return this.aspectRatio; }
            set {
                this.aspectRatio = value;
                this.projectionIsDirty = true;
            }
        }
        
        /// <summary>
        /// Gets or sets the field of view, in radians.
        /// </summary>
        public float FieldOfView {
            get { return this.fieldOfView; }
            set {
                this.fieldOfView = value;
                this.projectionIsDirty = true;
            }
        }

        /// <summary>
        /// Gets or sets the near plane distance.
        /// </summary>
        public float NearPlaneDistance {
            get { return this.nearPlaneDistance; }
            set {
                this.nearPlaneDistance = value;
                this.projectionIsDirty = true;
            }
        }

        /// <summary>
        ///  Gets or sets the far plane distance.
        /// </summary>
        public float FarPlaneDistance {
            get { return this.farPlaneDistance; }
            set {
                this.farPlaneDistance = value;
                this.projectionIsDirty = true;
            }
        }

        /// <summary>
        /// Gets or sets the position.
        /// </summary>
        public Vector3 Position {
            get {
                if (this.useReflectionValues)
                    return this.reflectionPosition;
                else
                    return this.cameraPosition; 
            }
            set {
                if (this.cameraPosition != value) {
                    this.cameraPosition = value;
                    this.viewIsDirty = true;
                    this.worldIsDirty = true;
                    if (this.PositionChanged != null)
                        this.PositionChanged(this, EventArgs.Empty);
                }

            }
        }

        /// <summary>
        /// Gets or sets the rotation.
        /// </summary>
        public Vector2 Rotation
        {
            get { return this.cameraRotation; }
            set
            {
                if (this.cameraRotation != value)
                {
                    this.cameraRotation = value;
                    this.viewIsDirty = true;
                    this.worldIsDirty = true;
                }
                
            }

        }

        /// <summary>
        /// Gets the projection matrix.
        /// </summary>
        public Matrix Projection {
            get {
                if (this.projectionIsDirty)
                    this.CreateProjection();
                return this.projection;
            }
        }

        /// <summary>
        /// Gets or sets the view matrix.
        /// </summary>
        public Matrix View {
            get {
                if (this.useReflectionValues)
                    return this.reflectionViewMatrix;
                else
                if (this.viewIsDirty)
                    this.CreateView();
                
                return this.view;
            }
            set { this.view = value; }
        }

        /// <summary>
        /// Gets the camera target.
        /// </summary>
        public Vector3 Target
        {
            get { return this.cameraTarget; }
        }

        /// <summary>
        /// Gets or sets the cameras up vector.
        /// </summary>
        public Vector3 Up {
            get { return this.cameraUp; }
            set {
                if (this.cameraUp != value) {
                    this.cameraUp = value;
                    this.viewIsDirty = true;
                }
            }
        }    
    }
}
